/**
 * Copyright 2007-2008 Vidar Svansson
 *     
 * This file is part of JAdapter.
 * 
 * JAdapter is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * JAdapter is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with JAdapter.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.jadapter;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.jadapter.adapters.AdapterBean;
import org.jadapter.exceptions.AdaptationException;

/** 
 * An "transformer adapter", <em>i.e.</em> an adapter that adapts 
 * classes that do not use the framework 
 * to the {@link org.jadapter.Transformer} interface.
 * <br></br><br></br>
 * This class uses reflection to determine instantiate the adapter
 * and appropriate method to use as the {@link org.jadapter.Transformer}.
 * This resolution requires type information, which can be injected.
 * <br></br><br></br>
 * The resolution only takes place the first time the adapter is used.
 * The {@link #resolved()} method can be used to check if a reflection resolution
 * has been instantiated for this adapter.
 * 
 * @author Vidar Svansson
 *
 * @param <I> The type provided by this object.
 * @param <C> The type this adapter can adapt.
 * 
 * @since 0.1
 */
final public class JAdapter<I, C> extends AdapterBean<I, C>  {

    /* The internal adapter, is created on demand or can be injected */
    private transient Transformer<I, C> transformer;

    /* The class that is turned into an adapter */
    private Class<?> factory;


    /**
     * Default no-arg constructor.
     * Type information and the adapter class will have to be injected 
     * after initialization.
     */
    public JAdapter() {
        // no type information available
    }

    /**
     * Create an adapter with all dependencies.
     */
    public JAdapter(Class<?> adapter, Class<I> to, Class<C> from) {
        setFactory(adapter);
        to(to);
        from(from);
    }

    /**
     * Create an adapter without type information.
     * @param adapter The adapter class ("the factory").
     */
    public JAdapter(Class<?> adapter) {
        setFactory(adapter);
    }


    public JAdapter(Transformer<I, C> adapter) {
        this.transformer = adapter;
    }        


    Query<Callable<I,C>> invokers = new Query<Callable<I,C>>(              
            new SingleArgInvoker(),
            new NoArgInvoker(),
            new VarArgInvoker()  
    );


    /** 
     * The implementation of the Adapter interface using reflection.
     * <br></br><br></br>
     * The algorithm to resolve the transformation is roughly as follows:
     * <ol>
     *  <li>If context is of type I, return it without any processing</li>
     *  <li>If a transformer instance is available, delegate the processing to it and return the result</li>
     *  <li>Use reflection to instantiate the factory class and inject the context and retrieve a result of type I</li>
     * </ol>
     */
    @Override
    @SuppressWarnings("unchecked")
    public I transform(C context) {
        // 1. Do we have the type already?
        // if I and C classes are not available then we can not check the type
        if (to() != null && to().isInstance(context))
            return (I) context;       

        Object instance = null;

        if (transformer != null) {           
            instance = transformer.transform(context);            
            return (I)instance;          
        } 


        try {           
            return invokers.xor(NoSuchMethodException.class).apply(context);    
        } catch (SecurityException e) {
            throw new AdaptationException("Constructor should be public", e);
        }        
        catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

    private final class SingleArgInvoker implements Callable<I,C> {
        public I apply(C context) throws Exception {

            Constructor<?> constructor = factory.getConstructor(from());          
            Object instance = constructor.newInstance(context);    

            if(to().isInstance(instance)){
                transformer = new SingleArgTransformer((Constructor<I>) constructor);
                return (I)instance;         
            } 

            throw new AdaptationException("Adapter is of incorrect type");           
        }
    }

    private final class NoArgInvoker implements Callable<I,C> {
        public I apply(C context) throws Exception  {          
            Constructor<?> constructor = factory.getConstructor();
            Object instance = constructor.newInstance();           


            if (instance instanceof Transformer) {
                return useTransformer(context, (Transformer<I,C>)instance);
            }  

            if(to().isInstance(instance)) {
                return useInjection(context, (I) instance);
            }
            return useFactory(context, instance); 

        }
    }

    private final class VarArgInvoker implements Callable<I,C> {
        public I apply(C context) throws Exception {
            for(Constructor<?> constructor : factory.getConstructors()) {
                Class<?>[] types = constructor.getParameterTypes();           
                if (constructor.isVarArgs() && from().isAssignableFrom(types[0])) {
                    if(types.length == 2) {
                        Object instance = constructor.newInstance(context, null);                                               
                        if(to().isInstance(instance)){
                            transformer = new VarArgTransformer((Constructor<I>) constructor);
                            return (I)instance;         
                        } 

                        throw new AdaptationException("Adapter is of incorrect type"); 
                    }                   
                }
            }  
            throw new AdaptationException("Unable to find any suitalbe constructor");
        }
    }


    private I useTransformer(C context, Transformer<I,C> transformer) {
        I result = transformer.transform(context);

        if (to().isInstance(result)) {
            this.transformer = transformer;
            return result; 
        } 

        throw new AdaptationException("Transformer is not suitable for this adapter. "
                + "Expected: " + from() + ", got " + result.getClass() );

    }


    private I useFactory(C context, Object instance) throws Exception {
        Class<?>[] types;
        Object result;

        for(Method m : instance.getClass().getDeclaredMethods()){
            types = m.getParameterTypes();
            if (types.length == 1 && types[0].isInstance(context) 
                    && to().isAssignableFrom(m.getReturnType())) {

                result = m.invoke(instance, context);
                if (to().isInstance(result)) {
                    transformer = new FactoryMethodTransformer(factory.getConstructor(), m);
                    return (I) result;
                }                      
            } 
        }        

        throw new AdaptationException("No suitable factory method found");             
    }

    private I useInjection(C context, I instance) throws Exception {
        Class<?>[] types;
        for(Method m : getFactory().getDeclaredMethods()){
            types = m.getParameterTypes();
            if (types.length == 1 && types[0].isInstance(context)) {

                m.invoke(instance, context);

                transformer = new InjectionMethodTransformer(factory.getConstructor(), m);
                return instance;             
            }   
        }
        throw new AdaptationException("No suitable injection method");

    }



    
    /**
     * 
     * @return true if an internal adapter has been initialized for this 
     * instance, <em>i.e. after the first call to it or it has been injected</em>. 
     * 
     */
    public boolean resolved() {
        return transformer != null;
    }
    
    
    class FactoryMethodTransformer implements Transformer<I,C> {
        Method method;
        Constructor constructor;

        FactoryMethodTransformer(Constructor constructor, Method method) {
            this.method = method;
            this.constructor = constructor;
        }

        public I transform(C context) {            
            try {
                Object instance = constructor.newInstance();
                Object o = method.invoke(instance, context);
                return (I)o;
            } catch (IllegalArgumentException e) {
                throw new AdaptationException("failed to create instance", e);               
            } catch (InstantiationException e) {
                throw new AdaptationException("failed to create instance", e);               
            } catch (IllegalAccessException e) {
                throw new AdaptationException("failed to create instance", e);               
            } catch (InvocationTargetException e) {
                throw new AdaptationException("failed to create instance", e);               
            }                                 
        }
       
    }
    
    class InjectionMethodTransformer implements Transformer<I,C>{
        Method method;
        Constructor constructor;

        InjectionMethodTransformer(Constructor constructor, Method method) {
            this.method = method;
            this.constructor = constructor;
        }

        public I transform(C context) {             
            try {
                Object instance = constructor.newInstance();
                Object o = method.invoke(instance, context);           
                    return (I)instance;
            } catch (IllegalArgumentException e) {
                throw new AdaptationException("failed to create instance", e);               
            } catch (InstantiationException e) {
                throw new AdaptationException("failed to create instance", e);               
            } catch (IllegalAccessException e) {
                throw new AdaptationException("failed to create instance", e);               
            } catch (InvocationTargetException e) {
                throw new AdaptationException("failed to create instance", e);               
            }                                
        }

    }   
    

    class SingleArgTransformer implements Transformer<I,C>{
        Constructor<I> constructor; 

        public SingleArgTransformer(Constructor<I> constructor){
            this.constructor = constructor;
        }

        public I transform(C item) {          
            try {
                return constructor.newInstance(item);
            } catch (Exception e) {
                throw new AdaptationException("SingleArgTransformer failed to create instance",e);               
            }                         
        }
    }
    
    class VarArgTransformer implements Transformer<I,C>{
        Constructor<I> constructor; 

        public VarArgTransformer(Constructor<I> constructor){
            this.constructor = constructor;
        }

        public I transform(C item) {          
            try {
                return constructor.newInstance(item, null);
            } catch (Exception e) {
                throw new AdaptationException("SingleArgTransformer failed to create instance",e);               
            }                         
        }
    }



    public static <I,C> JAdapter<I,C> NEW(Class<?> adapter, Class<I> provides, Class<C> context) {
        return new JAdapter<I,C>(adapter, provides, context);
    }


    public Class<?> getFactory() {
        return factory;
    }


    public void setFactory(Class<?> factory) {
        this.factory = factory;
    }   
    
    public Transformer<I, C> getTransformer() {
        return transformer;
    }

    public void setTransformer(Transformer<I, C> transformer) {
        this.transformer = transformer;
    }


}
